import asyncio
import re
import subprocess

from libqtile import bar
from libqtile.command.base import expose_command
from libqtile.log_utils import logger
from libqtile.utils import acall_process, create_task
from libqtile.widget import base

__all__ = [
    "Volume",
]

re_vol = re.compile(r"(\d?\d?\d?)%")


class Volume(base._TextBox):
    """Widget that display and change volume

    By default, this widget uses ``amixer`` to get and set the volume so users
    will need to make sure this is installed. Alternatively, users may set the
    relevant parameters for the widget to use a different application.

    If theme_path is set it draw widget as icons.
    """

    orientations = base.ORIENTATION_HORIZONTAL
    defaults = [
        ("cardid", None, "Card Id"),
        ("device", "default", "Device Name"),
        ("channel", "Master", "Channel"),
        ("padding", 3, "Padding left and right. Calculated if None."),
        ("update_interval", 0.2, "Update time in seconds."),
        ("theme_path", None, "Path of the icons"),
        (
            "emoji",
            False,
            "Use emoji to display volume states, only if ``theme_path`` is not set."
            "The specified font needs to contain the correct unicode characters.",
        ),
        (
            "emoji_list",
            ["\U0001f507", "\U0001f508", "\U0001f509", "\U0001f50a"],
            "List of emojis/font-symbols to display volume states, only if ``emoji`` is set."
            " List contains 4 symbols, from lowest volume to highest.",
        ),
        ("mute_command", None, "Mute command"),
        ("mute_foreground", None, "Foreground color for mute volume."),
        ("mute_format", "M", "Format to display when volume is muted."),
        ("unmute_format", "{volume}%", "Format of text to display when volume is not muted."),
        ("volume_app", None, "App to control volume"),
        ("volume_up_command", None, "Volume up command"),
        ("volume_down_command", None, "Volume down command"),
        (
            "get_volume_command",
            None,
            "Command to get the current volume. "
            "The expected output should include 1-3 numbers and a ``%`` sign.",
        ),
        ("check_mute_command", None, "Command to check mute status"),
        (
            "check_mute_string",
            "[off]",
            "String expected from check_mute_command when volume is muted."
            "When the output of the command matches this string, the"
            "audio source is treated as muted.",
        ),
        (
            "step",
            2,
            "Volume change for up an down commands in percentage."
            "Only used if ``volume_up_command`` and ``volume_down_command`` are not set.",
        ),
    ]

    def __init__(self, **config):
        base._TextBox.__init__(self, "", **config)
        self.add_defaults(Volume.defaults)
        self.surfaces = {}
        self.volume = None
        self.is_mute = False

        self.add_callbacks(
            {
                "Button1": self.mute,
                "Button3": self.run_app,
                "Button4": self.increase_vol,
                "Button5": self.decrease_vol,
            }
        )

    def timer_setup(self):
        create_task(self.do_volume())
        if self.theme_path:
            self.setup_images()

    def _configure(self, qtile, parent_bar):
        if self.theme_path:
            self.length_type = bar.STATIC
            self.length = 0
        base._TextBox._configure(self, qtile, parent_bar)
        self.unmute_foreground = self.foreground

    def create_amixer_command(self, *args) -> str:
        cmd = ["amixer"]

        if self.cardid is not None:
            cmd.extend(["-c", str(self.cardid)])

        if self.device is not None:
            cmd.extend(["-D", str(self.device)])

        cmd.extend([x for x in args])
        return subprocess.list2cmdline(cmd)

    def button_press(self, x, y, button):
        base._TextBox.button_press(self, x, y, button)
        self.draw()

    async def do_volume(self):
        vol, muted = await self.get_volume()
        if vol != self.volume or muted != self.is_mute:
            self.volume = vol
            self.is_mute = muted
            # Update the underlying canvas size before actually attempting
            # to figure out how big it is and draw it.
            self._update_drawer()
            self.bar.draw()
        await asyncio.sleep(self.update_interval)
        create_task(self.do_volume())

    def _update_drawer(self):
        if self.mute_foreground is not None:
            self.layout.colour = self.mute_foreground if self.is_mute else self.unmute_foreground

        if self.theme_path:
            self.drawer.clear(self.background or self.bar.background)
            if self.volume <= 0 or self.is_mute:
                img_name = "audio-volume-muted"
            elif self.volume <= 30:
                img_name = "audio-volume-low"
            elif self.volume < 80:
                img_name = "audio-volume-medium"
            else:  # self.volume >= 80:
                img_name = "audio-volume-high"

            self.drawer.ctx.set_source(self.surfaces[img_name])
            self.drawer.ctx.paint()
        elif self.emoji:
            if len(self.emoji_list) < 4:
                self.emoji_list = ["\U0001f507", "\U0001f508", "\U0001f509", "\U0001f50a"]
                logger.warning(
                    "Emoji list given has less than 4 items. Falling back to default emojis."
                )

            if self.volume <= 0 or self.is_mute:
                self.text = self.emoji_list[0]
            elif self.volume <= 30:
                self.text = self.emoji_list[1]
            elif self.volume < 80:
                self.text = self.emoji_list[2]
            elif self.volume >= 80:
                self.text = self.emoji_list[3]
        else:
            self.text = (
                self.mute_format if self.is_mute or self.volume < 0 else self.unmute_format
            ).format(volume=self.volume)

    def setup_images(self):
        from libqtile import images

        names = (
            "audio-volume-high",
            "audio-volume-low",
            "audio-volume-medium",
            "audio-volume-muted",
        )
        d_images = images.Loader(self.theme_path)(*names)
        new_height = self.bar.size - 2
        for name, img in d_images.items():
            img.resize(height=new_height)
            if img.width > self.length:
                self.length = img.width + self.padding * 2
            self.surfaces[name] = img.pattern

    async def get_volume(self):
        try:
            if self.get_volume_command is not None:
                get_volume_cmd = self.get_volume_command
            else:
                get_volume_cmd = self.create_amixer_command("sget", self.channel)

            mixer_out = await acall_process(get_volume_cmd, shell=True)
        except subprocess.CalledProcessError:
            return -1, False

        check_mute = mixer_out
        if self.check_mute_command:
            check_mute = await acall_process(self.check_mute_command, shell=True)

        muted = self.check_mute_string in check_mute

        volgroups = re_vol.search(mixer_out)
        if volgroups:
            return int(volgroups.groups()[0]), muted
        else:
            # this shouldn't happen
            return -1, muted

    def draw(self):
        if self.theme_path:
            self.draw_at_default_position()
        else:
            base._TextBox.draw(self)

    @expose_command()
    def increase_vol(self):
        if self.volume_up_command is not None:
            volume_up_cmd = self.volume_up_command
        else:
            volume_up_cmd = self.create_amixer_command(
                "-q", "sset", self.channel, f"{self.step}%+"
            )

        subprocess.call(volume_up_cmd, shell=True)

    @expose_command()
    def decrease_vol(self):
        if self.volume_down_command is not None:
            volume_down_cmd = self.volume_down_command
        else:
            volume_down_cmd = self.create_amixer_command(
                "-q", "sset", self.channel, f"{self.step}%-"
            )

        subprocess.call(volume_down_cmd, shell=True)

    @expose_command()
    def mute(self):
        if self.mute_command is not None:
            mute_cmd = self.mute_command
        else:
            mute_cmd = self.create_amixer_command("-q", "sset", self.channel, "toggle")

        subprocess.call(mute_cmd, shell=True)

    @expose_command()
    def run_app(self):
        if self.volume_app is not None:
            subprocess.Popen(self.volume_app, shell=True)
